<script>
  import {
    RelationshipType,
    PrettyRelationshipDefinitions,
  } from "@/constants/backend"
  import {
    keepOpen,
    Button,
    Input,
    ModalContent,
    Select,
    Detail,
    Body,
    Helpers,
  } from "@budibase/bbui"
  import { tables } from "@/stores/builder"
  import { RelationshipErrorChecker } from "./relationshipErrors"
  import { onMount } from "svelte"
  import RelationshipSelector from "@/components/common/RelationshipSelector.svelte"

  export let save
  export let datasource
  export let plusTables = []
  export let fromRelationship = {}
  export let toRelationship = {}
  export let selectedFromTable
  export let close

  let relationshipMap = {
    [RelationshipType.MANY_TO_MANY]: {
      part1: PrettyRelationshipDefinitions.MANY,
      part2: PrettyRelationshipDefinitions.MANY,
    },
    [RelationshipType.MANY_TO_ONE]: {
      part1: PrettyRelationshipDefinitions.MANY,
      part2: PrettyRelationshipDefinitions.ONE,
    },
    [RelationshipType.ONE_TO_MANY]: {
      part1: PrettyRelationshipDefinitions.ONE,
      part2: PrettyRelationshipDefinitions.MANY,
    },
  }
  $: relationshipOpts1 =
    relationshipPart2 === PrettyRelationshipDefinitions.ONE
      ? [PrettyRelationshipDefinitions.MANY]
      : Object.values(PrettyRelationshipDefinitions)

  $: relationshipOpts2 =
    relationshipPart1 === PrettyRelationshipDefinitions.ONE
      ? [PrettyRelationshipDefinitions.MANY]
      : Object.values(PrettyRelationshipDefinitions)

  let relationshipPart1 = PrettyRelationshipDefinitions.ONE
  let relationshipPart2 = PrettyRelationshipDefinitions.MANY

  let originalFromColumnName = toRelationship.name,
    originalToColumnName = fromRelationship.name
  let originalFromTable = plusTables.find(
    table => table._id === toRelationship?.tableId
  )
  let originalToTable = plusTables.find(
    table => table._id === fromRelationship?.tableId
  )

  let tableOptions
  let errorChecker = new RelationshipErrorChecker(
    invalidThroughTable,
    manyToManyRelationshipExistsFn
  )
  let errors = {}
  let fromPrimary, fromForeign, fromColumn, toColumn

  let throughId, throughToKey, throughFromKey
  let relationshipType
  let hasValidated = false

  $: fromId = null
  $: toId = null

  $: tableOptions = plusTables.map(table => ({
    label: table.name,
    value: table._id,
    name: table.name,
    _id: table._id,
  }))

  $: {
    // Determine the relationship type based on the selected values of both parts
    relationshipType = Object.entries(relationshipMap).find(
      ([_, parts]) =>
        parts.part1 === relationshipPart1 && parts.part2 === relationshipPart2
    )?.[0]

    changed(() => {
      hasValidated = false
    })
  }

  $: valid =
    getErrorCount(errors) === 0 &&
    allRequiredAttributesSet(relationshipType) &&
    fromId &&
    toId
  $: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
  $: isManyToOne =
    relationshipType === RelationshipType.MANY_TO_ONE ||
    relationshipType === RelationshipType.ONE_TO_MANY
  function getTable(id) {
    return plusTables.find(table => table._id === id)
  }

  function invalidThroughTable() {
    // need to know the foreign key columns to check error
    if (!throughId || !throughToKey || !throughFromKey) {
      return false
    }
    const throughTbl = plusTables.find(tbl => tbl._id === throughId)
    const otherColumns = Object.values(throughTbl.schema).filter(
      col => col.name !== throughFromKey && col.name !== throughToKey
    )
    for (let col of otherColumns) {
      if (col.constraints?.presence && !col.autocolumn) {
        return true
      }
    }
    return false
  }
  function manyToManyRelationshipExistsFn() {
    if (
      originalFromTable &&
      originalToTable &&
      originalFromTable === getTable(fromId) &&
      originalToTable === getTable(toId)
    ) {
      return false
    }
    let fromThroughLinks = Object.values(
      datasource.entities[getTable(fromId).name].schema
    ).filter(value => value.through)
    let toThroughLinks = Object.values(
      datasource.entities[getTable(toId).name].schema
    ).filter(value => value.through)

    const matchAgainstUserInput = link =>
      (link.throughTo === throughToKey &&
        link.throughFrom === throughFromKey) ||
      (link.throughTo === throughFromKey && link.throughFrom === throughToKey)

    const allLinks = [...fromThroughLinks, ...toThroughLinks]
    return !!allLinks.find(
      link => link.through === throughId && matchAgainstUserInput(link)
    )
  }

  function getErrorCount(errors) {
    return Object.entries(errors).filter(entry => !!entry[1]).length
  }

  function allRequiredAttributesSet(relationshipType) {
    const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
    if (relationshipType === RelationshipType.MANY_TO_ONE) {
      return base && fromPrimary && fromForeign
    } else {
      return base && getTable(throughId) && throughFromKey && throughToKey
    }
  }

  function validate() {
    if (!allRequiredAttributesSet(relationshipType) && !hasValidated) {
      return
    }

    hasValidated = true
    errorChecker.setType(relationshipType)
    const fromTable = getTable(fromId),
      toTable = getTable(toId),
      throughTable = getTable(throughId)
    errors = {
      relationshipType: errorChecker.relationshipTypeSet(relationshipType),
      fromTable:
        errorChecker.tableSet(fromTable) ||
        errorChecker.differentTables(fromId, toId, throughId),
      toTable:
        errorChecker.tableSet(toTable) ||
        errorChecker.differentTables(toId, fromId, throughId),
      throughTable:
        errorChecker.throughTableSet(throughTable) ||
        errorChecker.throughIsNullable() ||
        errorChecker.differentTables(throughId, fromId, toId) ||
        errorChecker.doesRelationshipExists(),
      throughFromKey:
        errorChecker.manyForeignKeySet(throughFromKey) ||
        errorChecker.manyTypeMismatch(
          fromTable,
          throughTable,
          fromTable.primary[0],
          throughToKey
        ) ||
        errorChecker.differentColumns(throughFromKey, throughToKey),
      throughToKey:
        errorChecker.manyForeignKeySet(throughToKey) ||
        errorChecker.manyTypeMismatch(
          toTable,
          throughTable,
          toTable.primary[0],
          throughFromKey
        ),
      fromForeign:
        errorChecker.foreignKeySet(fromForeign) ||
        errorChecker.typeMismatch(fromTable, toTable, fromPrimary, fromForeign),
      fromPrimary: errorChecker.primaryKeySet(fromPrimary),
      fromColumn: errorChecker.columnBeingUsed(
        toTable,
        fromColumn,
        originalFromColumnName
      ),
      toColumn: errorChecker.columnBeingUsed(
        fromTable,
        toColumn,
        originalToColumnName
      ),
    }
    return getErrorCount(errors) === 0
  }

  function otherRelationshipType(type) {
    if (type === RelationshipType.MANY_TO_ONE) {
      return RelationshipType.ONE_TO_MANY
    } else if (type === RelationshipType.ONE_TO_MANY) {
      return RelationshipType.MANY_TO_ONE
    } else if (type === RelationshipType.MANY_TO_MANY) {
      return RelationshipType.MANY_TO_MANY
    }
  }

  function buildRelationships() {
    const id = Helpers.uuid()
    //Map temporary variables
    let relateFrom = {
      ...fromRelationship,
      tableId: toId,
      name: toColumn,
      relationshipType,
      fieldName: fromForeign,
      through: throughId,
      throughFrom: throughFromKey,
      throughTo: throughToKey,
      type: "link",
      main: true,
      _id: id,
    }
    let relateTo = (toRelationship = {
      ...toRelationship,
      tableId: fromId,
      name: fromColumn,
      relationshipType: otherRelationshipType(relationshipType),
      through: throughId,
      type: "link",
      _id: id,
    })

    // if any to many only need to check from
    const manyToMany =
      relateFrom.relationshipType === RelationshipType.MANY_TO_MANY

    if (!manyToMany) {
      delete relateFrom.through
      delete relateTo.through
    }

    // [0] is because we don't support composite keys for relationships right now
    if (manyToMany) {
      relateFrom = {
        ...relateFrom,
        through: getTable(throughId)._id,
        fieldName: getTable(toId).primary[0],
      }
      relateTo = {
        ...relateTo,
        through: getTable(throughId)._id,
        fieldName: getTable(fromId).primary[0],
        throughFrom: relateFrom.throughTo,
        throughTo: relateFrom.throughFrom,
      }
    } else {
      // the relateFrom.fieldName should remain the same, as it is the foreignKey in the other
      // table, this is due to the way that budibase represents relationships, the fieldName in a
      // link column schema is the column linked to (FK in this case). The foreignKey column is
      // essentially what is linked to in the from table, this is unique to SQL as this isn't a feature
      // of Budibase internal tables.
      // Essentially this means the fieldName is what we are linking to in the other table, and the
      // foreignKey is what is linking out of the current table.
      relateFrom = {
        ...relateFrom,
        foreignKey: fromPrimary,
      }
      relateTo = {
        ...relateTo,
        relationshipType: RelationshipType.ONE_TO_MANY,
        foreignKey: relateFrom.fieldName,
        fieldName: fromPrimary,
      }
    }

    fromRelationship = relateFrom
    toRelationship = relateTo
  }

  function removeExistingRelationship() {
    if (originalFromTable && originalFromColumnName) {
      delete datasource.entities[originalFromTable.name].schema[
        originalToColumnName
      ]
    }
    if (originalToTable && originalToColumnName) {
      delete datasource.entities[originalToTable.name].schema[
        originalFromColumnName
      ]
    }
  }

  async function saveRelationship() {
    if (!validate()) {
      return keepOpen
    }
    buildRelationships()
    removeExistingRelationship()

    // source of relationship
    datasource.entities[getTable(fromId).name].schema[fromRelationship.name] =
      fromRelationship
    // save other side of relationship in the other schema
    datasource.entities[getTable(toId).name].schema[toRelationship.name] =
      toRelationship

    await save({ action: "saved" })
  }
  async function deleteRelationship() {
    removeExistingRelationship()
    await save({ action: "deleted" })
    await tables.fetch()
    close()
  }

  function changed(fn) {
    if (typeof fn === "function") {
      fn()
    }
    validate()
  }

  onMount(() => {
    if (fromRelationship) {
      fromPrimary = fromRelationship.foreignKey
      toId = fromRelationship.tableId
      throughId = fromRelationship.through
      throughFromKey = fromRelationship.throughFrom
      throughToKey = fromRelationship.throughTo
      toColumn = fromRelationship.name
    }
    if (toRelationship) {
      fromForeign = toRelationship.foreignKey
      fromId = toRelationship.tableId
      fromColumn = toRelationship.name
    }
    relationshipType =
      fromRelationship.relationshipType || RelationshipType.MANY_TO_ONE
    if (selectedFromTable) {
      fromId = selectedFromTable._id
      fromColumn = selectedFromTable.name
      fromPrimary = selectedFromTable?.primary[0] || null
    }
    if (relationshipType === RelationshipType.MANY_TO_MANY) {
      relationshipPart1 = PrettyRelationshipDefinitions.MANY
      relationshipPart2 = PrettyRelationshipDefinitions.MANY
    } else if (relationshipType === RelationshipType.MANY_TO_ONE) {
      relationshipPart1 = PrettyRelationshipDefinitions.ONE
      relationshipPart2 = PrettyRelationshipDefinitions.MANY
    } else {
      relationshipPart1 = PrettyRelationshipDefinitions.MANY
      relationshipPart2 = PrettyRelationshipDefinitions.ONE
    }
  })
</script>

<ModalContent
  title="Define Relationship"
  confirmText="Save"
  onConfirm={saveRelationship}
  disabled={!valid}
  size="L"
>
  <div class="headings">
    <Detail>Tables</Detail>
  </div>

  <RelationshipSelector
    bind:relationshipPart1
    bind:relationshipPart2
    bind:relationshipTableIdPrimary={fromId}
    bind:relationshipTableIdSecondary={toId}
    {relationshipOpts1}
    {relationshipOpts2}
    {tableOptions}
    {errors}
    primaryDisabled={selectedFromTable}
    primaryTableChanged={e =>
      changed(() => {
        const table = plusTables.find(tbl => tbl._id === e.detail)
        fromColumn = table?.name || ""
        fromPrimary = table?.primary?.[0]
      })}
    secondaryTableChanged={e =>
      changed(() => {
        const table = plusTables.find(tbl => tbl._id === e.detail)
        toColumn = table.name || ""
        fromForeign = null
      })}
  />

  {#if isManyToOne && fromId}
    <Select
      label={`Primary Key (${getTable(fromId).name})`}
      options={getTable(fromId).primary}
      bind:value={fromPrimary}
      bind:error={errors.fromPrimary}
      on:change={changed}
    />
  {/if}
  {#if isManyToMany}
    <Select
      label={"Through"}
      options={tableOptions}
      bind:value={throughId}
      bind:error={errors.throughTable}
      on:change={() =>
        changed(() => {
          throughToKey = null
          throughFromKey = null
        })}
    />
    {#if fromId && toId && throughId}
      <Select
        label={`Foreign Key (${getTable(fromId)?.name})`}
        options={Object.keys(getTable(throughId)?.schema)}
        bind:value={throughToKey}
        bind:error={errors.throughToKey}
        on:change={changed}
      />
      <Select
        label={`Foreign Key (${getTable(toId)?.name})`}
        options={Object.keys(getTable(throughId)?.schema)}
        bind:value={throughFromKey}
        bind:error={errors.throughFromKey}
        on:change={changed}
      />
    {/if}
  {:else if isManyToOne && toId}
    <Select
      label={`Foreign Key (${getTable(toId)?.name})`}
      options={Object.keys(getTable(toId)?.schema)}
      bind:value={fromForeign}
      bind:error={errors.fromForeign}
      on:change={changed}
    />
  {/if}
  <div class="headings">
    <Detail>Column names</Detail>
  </div>
  <Body>
    Budibase manages SQL relationships as a new column in the table, please
    provide a name for these columns.
  </Body>
  <Input
    label="From table column"
    bind:value={fromColumn}
    bind:error={errors.fromColumn}
    on:change={changed}
  />
  <Input
    label="To table column"
    bind:value={toColumn}
    bind:error={errors.toColumn}
    on:change={changed}
  />
  <div slot="footer">
    {#if originalFromColumnName != null}
      <Button warning text on:click={deleteRelationship}>Delete</Button>
    {/if}
  </div>
</ModalContent>

<style>
  .headings {
    margin-top: var(--spacing-s);
  }
</style>
